using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Client.System;
using Microsoft.Xna.Framework.Net;

namespace A2PongGame
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        public static GraphicsDeviceManager graphics;
        private Bat leftBat;
        private Bat rightBat;
        SpriteFont font;

        public static SpriteBatch spriteBatch { get; set; }

        public static int screenHeight { get; set; }
        public static int screenWidth { get; set; }

        public Input input;
        public static Ball ball { get; set; }

        public static Color bgColor { get; set; }

        public static int LeftPoints { get; set; }
        public static int RightPoints { get; set; }

        public Audio audio { get; set; }

        KeyboardState currentKeyboardState;
        GamePadState currentGamePadState;

        string errorMessage;

        public bool isFirstPlayer = true;

        //network stufff
        const int maxGamers = 16;
        const int maxLocalGamers = 2;

        NetworkSession networkSession;

        PacketWriter packetWriter = new PacketWriter();
        PacketReader packetReader = new PacketReader();
        
        public enum Side
        {
            Left,
            Right
        }
        
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            Components.Add(new GamerServicesComponent(this));
            
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            screenWidth = 800;
            screenHeight = 600;

            graphics.PreferredBackBufferWidth = screenWidth;
            graphics.PreferredBackBufferHeight = screenHeight;
            graphics.IsFullScreen = false;
            graphics.ApplyChanges();
            input = new Input();
            bgColor = Color.Tomato;
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            //leftBat = new Bat(this , Side.Left);
            //rightBat = new Bat(this, Side.Right);
            //ball = new Ball(this);
            ball = new Ball(this);
            myfont = Content.Load<SpriteFont>("UIFont");
            font = Content.Load<SpriteFont>("Font");
            console = new Console(this);

            //Components.Add(console);
            //Components.Add(leftBat);
            //Components.Add(rightBat);
            //Components.Add(ball);
            audio = new Audio();
            audio.LoadAudio(Content);
            //spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {

        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            HandleInput();

            if (networkSession == null)
            {
                // If we are not in a network session, update the
                // menu screen that will let us create or join one.
                UpdateMenuScreen();
            }
            else
            {
                // If we are in a network session, update it.
                UpdateNetworkSession(gameTime);
            }

            base.Update(gameTime);
        }

        void UpdateMenuScreen()
        {
            if (IsActive)
            {
                if (Gamer.SignedInGamers.Count == 0)
                {
                    // If there are no profiles signed in, we cannot proceed.
                    // Show the Guide so the user can sign in.
                    Guide.ShowSignIn(maxLocalGamers, false);
                }
                else if (IsPressed(Keys.A, Buttons.A))
                {
                    // Create a new session?
                    CreateSession();
                }
                else if (IsPressed(Keys.B, Buttons.B))
                {
                    // Join an existing session?
                    JoinSession();
                }
            }
        }

        /// <summary>
        /// Starts hosting a new network session.
        /// </summary>
        void CreateSession()
        {
            DrawMessage("Creating session...");

            try
            {
                networkSession = NetworkSession.Create(NetworkSessionType.SystemLink,
                                                       maxLocalGamers, maxGamers);

                HookSessionEvents();
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }

        /// <summary>
        /// Joins an existing network session.
        /// </summary>
        void JoinSession()
        {
            DrawMessage("Joining session...");

            try
            {
                // Search for sessions.
                using (AvailableNetworkSessionCollection availableSessions =
                            NetworkSession.Find(NetworkSessionType.SystemLink,
                                                maxLocalGamers, null))
                {
                    if (availableSessions.Count == 0)
                    {
                        errorMessage = "No network sessions found.";
                        return;
                    }

                    // Join the first session we found.
                    networkSession = NetworkSession.Join(availableSessions[0]);

                    HookSessionEvents();
                }
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }


        /// <summary>
        /// After creating or joining a network session, we must subscribe to
        /// some events so we will be notified when the session changes state.
        /// </summary>
        void HookSessionEvents()
        {
            networkSession.GamerJoined += GamerJoinedEventHandler;
            networkSession.SessionEnded += SessionEndedEventHandler;
        }


        /// <summary>
        /// This event handler will be called whenever a new gamer joins the session.
        /// We use it to allocate a Tank object, and associate it with the new gamer.
        /// </summary>
        void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e)
        {
            int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer);

            if (gamerIndex == 0)
                e.Gamer.Tag = new Bat(this, Side.Left);
            else
            {
                e.Gamer.Tag = new Bat(this, Side.Right);
                
            }
            
        }


        /// <summary>
        /// Event handler notifies us when the network session has ended.
        /// </summary>
        void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e)
        {
            errorMessage = e.EndReason.ToString();

            networkSession.Dispose();
            networkSession = null;
        }

        /// <summary>
        /// Updates the state of the network session, moving the tanks
        /// around and synchronizing their state over the network.
        /// </summary>
        void UpdateNetworkSession(GameTime gameTime)
        {
            // Read inputs for locally controlled tanks, and send them to the server.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                UpdateLocalGamer(gamer, gameTime);
            }

            // If we are the server, update all the tanks and transmit
            // their latest positions back out over the network.
            if (networkSession.IsHost)
            {
                UpdateServer(gameTime);
            }

            // Pump the underlying session object.
            networkSession.Update();

            // Make sure the session has not ended.
            if (networkSession == null)
                return;

            // Read any incoming network packets.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                if (gamer.IsHost)
                {
                    ServerReadInputFromClients(gamer, gameTime);
                }
                else
                {
                    ClientReadGameStateFromServer(gamer, gameTime);
                }
            }
        }

        /// <summary>
        /// Helper for updating a locally controlled gamer.
        /// </summary>
        void UpdateLocalGamer(LocalNetworkGamer gamer, GameTime gameTime)
        {
            // Look up what tank is associated with this local player,
            // and read the latest user inputs for it. The server will
            // later use these values to control the tank movement.
            Bat localBat = gamer.Tag as Bat;

            ReadBatInputs(localBat, gamer.SignedInGamer.PlayerIndex);
            //localBat.Update(gameTime);
            // Only send if we are not the server. There is no point sending packets
            // to ourselves, because we already know what they will contain!
            if (!networkSession.IsHost)
            {
                
                // Write our latest input state into a network packet.
                packetWriter.Write(localBat.rectSource.Y);
                packetWriter.Write(ball.rectSource.X);
                packetWriter.Write(ball.rectSource.Y);
                packetWriter.Write(ball.speedX);
                packetWriter.Write(ball.speedY);

                // Send our input data to the server.
                gamer.SendData(packetWriter,
                               SendDataOptions.InOrder, networkSession.Host);
            }
        }


        /// <summary>
        /// This method only runs on the server. It calls Update on all the
        /// tank instances, both local and remote, using inputs that have
        /// been received over the network. It then sends the resulting
        /// tank position data to everyone in the session.
        /// </summary>
        void UpdateServer(GameTime gameTime)
        {
            ball.Update(gameTime);
            // Loop over all the players in the session, not just the local ones!
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                // Look up what tank is associated with this player.
                Bat bat = gamer.Tag as Bat;

                // Update the tank.
                bat.Update(gameTime);
                

                // Write the tank state into the output network packet.
                packetWriter.Write(gamer.Id);
                packetWriter.Write(bat.rectSource.Y);
                packetWriter.Write(ball.rectSource.X);
                packetWriter.Write(ball.rectSource.Y);
                packetWriter.Write(ball.speedX);
                packetWriter.Write(ball.speedY);
            }

            // Send the combined data for all tanks to everyone in the session.
            LocalNetworkGamer server = (LocalNetworkGamer)networkSession.Host;

            server.SendData(packetWriter, SendDataOptions.InOrder);
        }


        /// <summary>
        /// This method only runs on the server. It reads tank inputs that
        /// have been sent over the network by a client machine, storing
        /// them for later use by the UpdateServer method.
        /// </summary>
        void ServerReadInputFromClients(LocalNetworkGamer gamer, GameTime gameTime)
        {
            // Keep reading as long as incoming packets are available.
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                gamer.ReceiveData(packetReader, out sender);

                if (!sender.IsLocal)
                {
                    // Look up the tank associated with whoever sent this packet.
                    Bat remoteBat = sender.Tag as Bat;

                    // Read the latest inputs controlling this tank
                    remoteBat.setY(packetReader.ReadInt32());
                    ball.setXY(packetReader.ReadInt32(), packetReader.ReadInt32());
                    ball.speedX = packetReader.ReadInt32();
                    ball.speedY = packetReader.ReadInt32();
                }
            }
        }


        /// <summary>
        /// This method only runs on client machines. It reads
        /// tank position data that has been computed by the server.
        /// </summary>
        void ClientReadGameStateFromServer(LocalNetworkGamer gamer, GameTime gameTime)
        {
            // Keep reading as long as incoming packets are available.
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                gamer.ReceiveData(packetReader, out sender);

                // This packet contains data about all the players in the session.
                // We keep reading from it until we have processed all the data.
                while (packetReader.Position < packetReader.Length)
                {
                    // Read the state of one tank from the network packet.
                    byte gamerId = packetReader.ReadByte();

                    int y = packetReader.ReadInt32();
                    int ballx = packetReader.ReadInt32();
                    int bally = packetReader.ReadInt32();
                    int ballspeedx = packetReader.ReadInt32();
                    int ballspeedy = packetReader.ReadInt32();

                    // Look up which gamer this state refers to.
                    NetworkGamer remoteGamer = networkSession.FindGamerById(gamerId);

                    // This might come back null if the gamer left the session after
                    // the host sent the packet but before we received it. If that
                    // happens, we just ignore the data for this gamer.
                    if (remoteGamer != null)
                    {
                        // Update our local state with data from the network packet.
                        Bat bat = remoteGamer.Tag as Bat;

                        bat.setY(y);
                        ball.setXY(ballx, bally);
                        ball.speedX = ballspeedx;
                        ball.speedY = ballspeedy;
                    }
                }
            }
        }


        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            if (networkSession == null)
            {
                // If we are not in a network session, draw the
                // menu screen that will let us create or join one.
                DrawMenuScreen();
            }
            else
            {
                // If we are in a network session, draw it.
                DrawNetworkSession(gameTime);
            }

            base.Draw(gameTime);

            //GraphicsDevice.Clear(bgColor);
           
            //spriteBatch.Begin();
            //spriteBatch.DrawString(myfont,"Player 1 : " + LeftPoints + "   Player 2 : " + RightPoints , new Vector2((screenWidth * .3F) ,0), Color.White);
            //spriteBatch.End();

            //base.Draw(gameTime);
        }

        /// <summary>
        /// Draws the startup screen used to create and join network sessions.
        /// </summary>
        void DrawMenuScreen()
        {
            string message = string.Empty;

            if (!string.IsNullOrEmpty(errorMessage))
                message += "Error:\n" + errorMessage.Replace(". ", ".\n") + "\n\n";

            message += "A = create session\n" +
                       "B = join session";

            spriteBatch.Begin();

            spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
            spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);

            spriteBatch.End();
        }

        /// <summary>
        /// Draws the state of an active network session.
        /// </summary>
        void DrawNetworkSession(GameTime gameTime)
        {
           // spriteBatch.Begin();

            // For each person in the session...
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                // Look up the tank object belonging to this network gamer.
                Bat bat = gamer.Tag as Bat;

                // Draw the tank.
                bat.Draw(gameTime);


            }
            ball.Draw(gameTime);
            //spriteBatch.End();
        }


        /// <summary>
        /// Helper draws notification messages before calling blocking network methods.
        /// </summary>
        void DrawMessage(string message)
        {
            if (!BeginDraw())
                return;

            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
            spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);

            spriteBatch.End();

            EndDraw();
        }

        private void HandleInput()
        {
            currentKeyboardState = Keyboard.GetState();
            currentGamePadState = GamePad.GetState(PlayerIndex.One);

            // Check for exit.
            if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
            {
                Exit();
            }
        }

        /// <summary>
        /// Checks if the specified button is pressed on either keyboard or gamepad.
        /// </summary>
        bool IsPressed(Keys key, Buttons button)
        {
            return (currentKeyboardState.IsKeyDown(key) ||
                    currentGamePadState.IsButtonDown(button));
        }

        /// <summary>
        /// Reads input data from keyboard and gamepad, and stores
        /// it into the specified tank object.
        /// </summary>
        void ReadBatInputs(Bat bat, PlayerIndex playerIndex)
        {
            // Read the gamepad.
            GamePadState gamePad = GamePad.GetState(playerIndex);

            // Read the keyboard.
            KeyboardState keyboard = Keyboard.GetState(playerIndex);

            if (keyboard.IsKeyDown(Keys.Up) || gamePad.IsButtonDown(Buttons.DPadUp))
                bat.DeltaY -= 5;

            if (keyboard.IsKeyDown(Keys.Down) || gamePad.IsButtonDown(Buttons.DPadDown))
                bat.DeltaY += 5;

        }

        public Console console { get; set; }

        public static SpriteFont myfont { get; set; }
    }
}
